#include	"script.h"
#include	"logger.h"
#include	"scheduler.h"
#include	"string_tools.h"
#include	<map>
#include	<stdexcept>

struct LuaPrinter {

	static int Print(lua_State * L) {
		std::string out;
		out.reserve(2048);

		int n = lua_gettop(L);  /* number of arguments */
		int i;

		lua_Debug d;
		memset(&d, 0, sizeof(d));

		lua_getstack(L, 1, &d);
		lua_getinfo(L, "Sln", &d);
		lua_getglobal(L, "tostring");

		for (i = 1; i <= n; i++) {
			const char *s;
			size_t l;
			lua_pushvalue(L, -1);  /* function to be called */
			lua_pushvalue(L, i);   /* value to print */
			lua_call(L, 1, 1);
			s = lua_tolstring(L, -1, &l);  /* get result */
			if (s == NULL)
				return luaL_error(L, "'tostring' must return a string to 'print'");

			if (i > 1) out.push_back('\t');
			out.append(s, l);
			lua_pop(L, 1);  /* pop result */
		}

		Logger::Instance().Write(Logger::Info, d.short_src, d.currentline, "%s", out.c_str());
		return 0;
	}

	static int PrintError(lua_State * L) {
		std::string out;
		out.reserve(2048);

		int n = lua_gettop(L);  /* number of arguments */
		int i;

		lua_Debug d;
		memset(&d, 0, sizeof(d));

		lua_getstack(L, 1, &d);
		lua_getinfo(L, "Sln", &d);
		lua_getglobal(L, "tostring");

		for (i = 1; i <= n; i++) {
			const char *s;
			size_t l;
			lua_pushvalue(L, -1);  /* function to be called */
			lua_pushvalue(L, i);   /* value to print */
			lua_call(L, 1, 1);
			s = lua_tolstring(L, -1, &l);  /* get result */
			if (s == NULL)
				return luaL_error(L, "'tostring' must return a string to 'print'");

			if (i > 1) out.push_back('\t');
			out.append(s, l);
			lua_pop(L, 1);  /* pop result */
		}

		Logger::Instance().Write(Logger::Error, d.short_src, d.currentline, "%s", out.c_str());
		return 0;
	}
};

struct LuaBits {

	static int Load(lua_State * L) {
		int top = lua_gettop(L);
		if (top < 3) luaL_error(L, "loadbits(int32, start, end) bad param count!!!");

		uint32_t src = 0;
		int start = 0, end = 0;
		
		if (!lua_isinteger(L, 1)) luaL_error(L, "loadbits(int32, start, end) #1 must be 32-bit integer!!!");
		if (!lua_isinteger(L, 2)) luaL_error(L, "loadbits(int32, start, end) #2 must be integer between [0, 31]!!!");
		if (!lua_isinteger(L, 3)) luaL_error(L, "loadbits(int32, start, end) #3 must be integer between [0, 31]!!!");

		src = (uint32_t)lua_tointeger(L, 1);
		start = (int)lua_tointeger(L, 2);
		end = (int)lua_tointeger(L, 3);

		if (start < 0 || start > 31) luaL_error(L, "loadbits(int32, start, end) #2 must be integer between [0, 31]!!!");
		if (end < 0 || end > 31) luaL_error(L, "loadbits(int32, start, end) #3 must be integer between [0, 31]!!!");

		if (start > end) {
			int temp = start;
			start = end;
			end = temp;
		}

		uint64_t mask = (1 << (end - start + 1)) - 1;
		src = ((src >> start) & (uint32_t)(mask & 0xFFFFFFFF));
		lua_pushinteger(L, (lua_Integer)src);
		return 1;
	}

	static int Save(lua_State * L) {
		int top = lua_gettop(L);
		if (top < 4) luaL_error(L, "savebits(int32, start, end, value) bad param count!!!");

		uint32_t src = 0, val = 0;
		int start = 0, end = 0;

		if (!lua_isinteger(L, 1)) luaL_error(L, "savebits(int32, start, end, value) #1 must be 32-bit integer!!!");
		if (!lua_isinteger(L, 2)) luaL_error(L, "savebits(int32, start, end, value) #2 must be integer between [0, 31]!!!");
		if (!lua_isinteger(L, 3)) luaL_error(L, "savebits(int32, start, end, value) #3 must be integer between [0, 31]!!!");
		if (!lua_isinteger(L, 4)) luaL_error(L, "savebits(int32, start, end, value) #4 must be 32-bit integer!!!");

		src = (uint32_t)lua_tointeger(L, 1);
		start = (int)lua_tointeger(L, 2);
		end = (int)lua_tointeger(L, 3);
		val = (uint32_t)lua_tointeger(L, 4);

		if (start < 0 || start > 31) luaL_error(L, "savebits(int32, start, end, value) #2 must be integer between [0, 31]!!!");
		if (end < 0 || end > 31) luaL_error(L, "savebits(int32, start, end, value) #3 must be integer between [0, 31]!!!");

		if (start > end) {
			int temp = start;
			start = end;
			end = temp;
		}

		uint64_t mask = (1 << (end - start + 1)) - 1;
		uint32_t clear = ~((uint32_t)(mask & 0xFFFFFFFF) << start);

		val = val & (uint32_t)(mask & 0xFFFFFFFF);
		val = val << start;

		src = ((src & clear) | val);
		lua_pushinteger(L, (lua_Integer)src);
		return 1;
	}
};

struct LuaScheduler {
	static std::map<uint64_t, int> jobs;

	static void __Proc(uint64_t id, bool unref) {
		lua_State * L = GLua.State();
		auto it = jobs.find(id);
		if (it == jobs.end()) return;

		int ref = it->second;
		int top = lua_gettop(L);
		lua_getglobal(L, "debug");
		lua_getfield(L, -1, "traceback");
		lua_remove(L, -2);

		lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
		if (0 != lua_pcall(L, 0, 0, top + 1)) {
			lua_remove(L, top + 1);
			lua_error(L);
		}

		if (unref) {
			jobs.erase(it);
			luaL_unref(L, LUA_REGISTRYINDEX, ref);
		}

		lua_settop(L, top);
	}

	static int AddTimer(lua_State * L) {
		double delay = (double)lua_tonumber(L, 1);
		bool loop = (lua_gettop(L) > 2) ? (lua_toboolean(L, 3) == 1) : false;

		lua_pushvalue(L, 2);
		if (!lua_isfunction(L, -1)) luaL_error(L, "scheduler.timer(double,func[,loop]) #2 must be callable function!!!");
		int ref = luaL_ref(L, LUA_REGISTRYINDEX);
		uint64_t id = GScheduler.Add(delay, [=](uint64_t job_id) { __Proc(job_id, !loop); }, loop);
		jobs[id] = ref;
		lua_pushinteger(L, (lua_Integer)id);
		return 1;
	}

	static int AddTask(lua_State * L) {
		int hour = (int)lua_tointeger(L, 1);
		int minute = (int)lua_tointeger(L, 2);
		int second = (int)lua_tointeger(L, 3);

		lua_pushvalue(L, 4);
		if (!lua_isfunction(L, -1)) luaL_error(L, "scheduler.task(hour,min,sec,func) #2 must be callable function!!!");
		int ref = luaL_ref(L, LUA_REGISTRYINDEX);
		uint64_t id = GScheduler.Add(hour, minute, second, [](uint64_t job_id) { __Proc(job_id, false); });
		jobs[id] = ref;
		lua_pushinteger(L, (lua_Integer)id);
		return 1;
	}

	static int IsValid(lua_State * L) {
		uint64_t id = lua_tointeger(L, 1);
		lua_pushboolean(L, GScheduler.IsValid(id) ? 1 : 0);
		return 1;
	}

	static int Cancel(lua_State * L) {
		uint64_t id = lua_tointeger(L, 1);
		auto it = jobs.find(id);
		if (it != jobs.end()) luaL_unref(L, LUA_REGISTRYINDEX, it->second);
		GScheduler.Cancel(id);
		return 0;
	}

	static int GetLeftTime(lua_State * L) {
		uint64_t id = lua_tointeger(L, 1);
		lua_pushnumber(L, (lua_Number)GScheduler.GetRemainTime(id));
		return 1;
	}
};

std::map<uint64_t, int> LuaScheduler::jobs;

int LuaProxy::Readonly(lua_State * L) {
	return luaL_error(L, "Property '%s' is read-only!!!", lua_tostring(L, lua_upvalueindex(1)));
}

int LuaProxy::Index(lua_State * L) {
	lua_pushvalue(L, 2);	// tb, key, key
	lua_rawget(L, 1);		// tb, key, tb[key]
	if (lua_isfunction(L, -1)) return 1;
	lua_pop(L, 1);

	lua_getmetatable(L, 1);	// tb, key, meta
	lua_pushvalue(L, 2);	// tb, key, meta, key
	lua_rawget(L, -2);		// tb, key, meta, meta[key]

	if (lua_isnil(L, -1)) {
		lua_pop(L, 1);					// tb, key, meta
		lua_pushstring(L, "__propget");	// tb, key, meta, '__propget'
		lua_rawget(L, -2);				// tb, key, meta, meta.__propget
		lua_pushvalue(L, 2);			// tb, key, meta, meta.__propget, key
		lua_rawget(L, -2);				// tb, key, meta, meta.__propget, meta.__propget[key]
		lua_remove(L, -2);				// tb, key, meta, meta.__propget[key]

		if (lua_iscfunction(L, -1)) {
			lua_remove(L, -2);
			lua_pushvalue(L, 2);
			lua_call(L, 1, 1);
			return 1;
		} else {
			lua_pop(L, 2);
			lua_pushnil(L);
			return 0;
		}
	} else if (lua_istable(L, -1) || lua_iscfunction(L, -1) || lua_isfunction(L, -1)) {
		lua_remove(L, -2);
		return 1;
	} else {
		lua_pop(L, 2);
		return 0;
	}
}

int LuaProxy::NewIndex(lua_State * L) {
	if (lua_isfunction(L, -1)) {
		lua_rawset(L, -3);
		return 0;
	}

	lua_getmetatable(L, 1);				// tb, key, value, meta
	lua_pushstring(L, "__propset");		// tb, key, value, meta, '__propset'
	lua_rawget(L, -2);					// tb, key, value, meta, meta.__propset
	lua_remove(L, -2);					// tb, key, value, meta.__propset,

	if (!lua_istable(L, -1)) {
		lua_pop(L, 1);
		return 0;
	}

	lua_pushvalue(L, 2);	//! tb, key, value, meta.__propset, key
	lua_rawget(L, -2);		//! tb, key, value, meta.__propset, meta.__propset.key
	lua_remove(L, -2);		//! tb, key, value, meta.__propset.key

	if (lua_iscfunction(L, -1)) {
		lua_pushvalue(L, 3);	//! tb, key, value, meta.__propset.key, value
		lua_call(L, 1, 0);
	}

	return 0;
}

int LuaProxy::Method(lua_State * L) {
	if (!lua_islightuserdata(L, lua_upvalueindex(1))) luaL_error(L, "Call method error!!!");

	typedef int(*MethodFunc)(LuaStack &);
	MethodFunc f = (MethodFunc)(lua_touserdata(L, lua_upvalueindex(1)));
	if (!f) luaL_error(L, "Try to call an undefined method!!!");

	LuaStack _(L, 0);
	return f(_);
}

LuaTable & LuaTable::operator=(const LuaTable & r) {
	__Unref();

	if (r._L && r._ref != LUA_NOREF) {
		lua_rawgeti(r._L, LUA_REGISTRYINDEX, r._ref);
		_L = r._L;
		_ref = luaL_ref(_L, LUA_REGISTRYINDEX);
	}

	return *this;
}

int LuaTable::Size() const {
	if (!IsValid()) return 0;
	lua_rawgeti(_L, LUA_REGISTRYINDEX, _ref);
	int n = (int)luaL_len(_L, -1);
	lua_pop(_L, 1);
	return n;
}

void LuaTable::__Unref() {
	if (_L && _ref != LUA_NOREF) luaL_unref(_L, LUA_REGISTRYINDEX, _ref);
	_L = 0;
	_ref = LUA_NOREF;
}

LuaNamespace & LuaNamespace::Method(const std::string & name, int (*func)(LuaStack &)) {
	int top = lua_gettop(_L);

	lua_rawgeti(_L, LUA_REGISTRYINDEX, _ref);
	lua_pushlstring(_L, name.c_str(), name.size());
	lua_pushlightuserdata(_L, (void*)func);
	lua_pushcclosure(_L, &LuaProxy::Method, 1);
	lua_rawset(_L, -3);
	lua_settop(_L, top);

	return *this;
}

LuaVM::LuaVM() : _L(luaL_newstate()) {
	luaL_openlibs(_L);

	/// Hook error using Logger.
	lua_atpanic(_L, [](lua_State * L) -> int {
		if (lua_gettop(L) > 0) {
			Logger::Instance().Write(Logger::Error, "@SCRIPT", -1, lua_tostring(L, -1));
			lua_pop(L, 1);
		} else {
			Logger::Instance().Write(Logger::Error, "@SCRIPT", -1, "Unknown error in script!!");
		}

		lua_gc(L, LUA_GCCOLLECT, 0);
		throw std::runtime_error("Script error! Just for long jump");
		return 0;
	});

	/// Override print using Logger interface.
	lua_pushcfunction(_L, &LuaPrinter::Print);
	lua_setglobal(_L, "print");
	lua_pushcfunction(_L, &LuaPrinter::PrintError);
	lua_setglobal(_L, "print_err");

	/// Add bit operation tools.
	lua_pushcfunction(_L, &LuaBits::Load);
	lua_setglobal(_L, "loadbits");
	lua_pushcfunction(_L, &LuaBits::Save);
	lua_setglobal(_L, "savebits");

	/// Add scheduler interface.
	lua_newtable(_L);
	lua_pushcfunction(_L, &LuaScheduler::AddTimer);
	lua_setfield(_L, -2, "timer");
	lua_pushcfunction(_L, &LuaScheduler::AddTask);
	lua_setfield(_L, -2, "task");
	lua_pushcfunction(_L, &LuaScheduler::Cancel);
	lua_setfield(_L, -2, "cancel");
	lua_pushcfunction(_L, &LuaScheduler::IsValid);
	lua_setfield(_L, -2, "is_valid");
	lua_pushcfunction(_L, &LuaScheduler::GetLeftTime);
	lua_setfield(_L, -2, "remain");
	lua_setglobal(_L, "scheduler");
}

LuaVM & LuaVM::Instance() {
	static LuaVM * GIns = nullptr;
	if (GIns == nullptr) GIns = new LuaVM();
	return *GIns;
}

void LuaVM::DoFile(const std::string & path, bool require /* = false */) {
	if (path.length() <= 4) return;

	if (require) {
		std::string temp(path);
		std::string ext = ToUpper(temp.substr(temp.size() - 4));

		if (ext == ".LUA") temp = temp.substr(0, temp.size() - 4);
		
		std::string module = "";
		size_t size = 0;

		for (size_t i = 0; i < temp.size(); ++i) {
			char c = temp[i];

			if (c == '\\' || c == '/' || c == '.') {
				if (size > 0 && module[size - 1] == '.') continue;
				module.push_back('.');
				size++;
			} else {
				module.push_back(c);
				size++;
			}
		}

		Run("require '" + module + "'");
	} else {
		int top = lua_gettop(_L);
		
		lua_getglobal(_L, "debug");
		lua_getfield(_L, -1, "traceback");
		lua_remove(_L, -2);

		try {
			if (0 != luaL_loadfile(_L, path.c_str()) || 0 != lua_pcall(_L, 0, LUA_MULTRET, top + 1)) {
				lua_remove(_L, top + 1);
				lua_error(_L);
			}
		} catch (...) {}

		lua_settop(_L, top);
	}
}

void LuaVM::Run(const std::string & code) {
	int top = lua_gettop(_L);

	lua_getglobal(_L, "debug");
	lua_getfield(_L, -1, "traceback");
	lua_remove(_L, -2);

	try {
		if (0 != luaL_loadstring(_L, code.c_str()) || 0 != lua_pcall(_L, 0, 0, top + 1)) {
			lua_remove(_L, top + 1);
			lua_error(_L);
		}
	} catch (...) {}

	lua_settop(_L, top);
}

LuaNamespace LuaVM::Register(const std::string & table_name) {
	lua_getglobal(_L, table_name.c_str());
	if (!lua_isnil(_L, -1)) return LuaNamespace(_L, luaL_ref(_L, LUA_REGISTRYINDEX));

	lua_pop(_L, 1);
	lua_newtable(_L);
	lua_newtable(_L);

	lua_pushcfunction(_L, &LuaProxy::Index);
	lua_setfield(_L, -2, "__index");

	lua_pushcfunction(_L, &LuaProxy::NewIndex);
	lua_setfield(_L, -2, "__newindex");

	lua_newtable(_L);
	lua_setfield(_L, -2, "__propget");

	lua_newtable(_L);
	lua_setfield(_L, -2, "__propset");

	lua_setmetatable(_L, -2);
	lua_setglobal(_L, table_name.c_str());
	lua_getglobal(_L, table_name.c_str());
	return LuaNamespace(_L, luaL_ref(_L, LUA_REGISTRYINDEX));
}
